在 Elixir 中,进程默认是短暂的;一旦函数执行完毕,进程就会终止。要创建一个 持久化、有状态的进程,我们需要使用递归来使进程在循环中保持活跃。
1. 尾调用优化(TCO)
如果一个函数的绝对最后操作是调用自身,Erlang 虚拟机(BEAM)将执行 尾调用优化。它不会向栈中添加新的帧,而是直接 跳回到函数的起始位置 并使用新参数重新开始执行。
def factorial(n, acc), do: _fact(n-1, acc*n) # TCO
def factorial(n), do: n * factorial(n-1) # 非 TCO
def factorial(n), do: n * factorial(n-1) # 非 TCO
2. 持久化状态
状态通过将更新后的值作为参数传递给递归调用进行维护。由于尾调用优化,这些参数会替换栈上的原始参数,而无需额外内存开销,从而允许循环无限运行。
main.py
TERMINALbash — 80x24
> Ready. Click "Run" to execute.
>
QUESTION 1
What is the primary requirement for Tail-Call Optimization to occur?
The function must use the 'loop' keyword.
The recursive call must be the absolute final expression executed.
The function must be defined inside a module.
Arguments must be integers.
✅ Correct!
Correct. If any operation occurs after the call (like multiplication), the frame remains on the stack, leading to exhaustion.❌ Incorrect
TCO requires that no work remains after the recursive call so the VM can safely jump back to the start.QUESTION 2
Exercise: WorkingWithMultipleProcesses-1. Run the Spawn1 and Spawn4 code. See if you get comparable results. What is the observable difference?
Spawn1 crashes after one message; Spawn4 stays alive.
Spawn1 is faster than Spawn4.
Spawn4 uses more memory over time.
There is no observable difference.
✅ Correct!
Reference Answer: When running spawn1.exs, the process handles one message and dies. In spawn4.exs, the process recurses, remaining in the process list and capable of responding to subsequent messages.❌ Incorrect
Check the process lifecycle. One-off processes terminate immediately after their function block finishes.QUESTION 3
In pmap, we assign 'self' to 'me' before spawning. Why?
To make the code more readable.
Because 'self' inside a spawn block refers to the child process PID.
To bypass the Pin operator requirements.
✅ Correct!
Correct! If you used 'self' inside the child's block, the child would try to send the message to itself, not the parent.❌ Incorrect
Remember that 'self()' is a dynamic call; its value depends on which process is currently executing the code.QUESTION 4
Use spawn_link to start a child that sends a message and exits. If the parent sleeps for 500ms before receiving, what happens to the message?
The message is lost because the parent wasn't waiting.
The message is stored in the parent's mailbox until processed.
The parent crashes immediately.
✅ Correct!
Reference Answer: The message is placed in the mailbox. It does not matter that you weren't waiting; the BEAM buffers incoming messages. If trapping exits, you would receive both the custom message and the {:EXIT, pid, :normal} signal.❌ Incorrect
Elixir processes have mailboxes that act as buffers for incoming data.QUESTION 5
Is the order of replies in concurrent processes deterministic in theory?
Yes, they always arrive in spawn order.
No, it depends on the scheduler and execution time.
✅ Correct!
Correct. In practice, we use the Pin operator (^pid) to ensure we receive messages in a specific, deterministic order regardless of completion speed.❌ Incorrect
Concurrent execution is inherently non-deterministic. We must enforce order in our code logic.Case Study: Token Exchange Persistence
WorkingWithMultipleProcesses-2
You need to spawn two processes that receive unique tokens ('fred' and 'betty') and send them back. You must ensure the results are gathered in the correct order even if the processes finish at different times.
Q
How can you make the order of received tokens deterministic in practice?
Solution:
By capturing the PIDs of the spawned processes and using the Pin operator (^pid) in the receive block. This forces the parent to wait for the message from a specific PID before moving on to the next, regardless of which message is at the top of the mailbox.
By capturing the PIDs of the spawned processes and using the Pin operator (^pid) in the receive block. This forces the parent to wait for the message from a specific PID before moving on to the next, regardless of which message is at the top of the mailbox.
Q
Write the logical structure for a persistent 'Token' receiver using TCO.
Solution:
The receiver should be a function (e.g., 'loop') that contains a 'receive' block. After processing the token and sending it back, it must call 'loop()' as the final statement. This allows the process to handle multiple tokens over its lifetime without crashing the stack.
The receiver should be a function (e.g., 'loop') that contains a 'receive' block. After processing the token and sending it back, it must call 'loop()' as the final statement. This allows the process to handle multiple tokens over its lifetime without crashing the stack.